07 - 失败模式、评测与选型
本篇回答:这一层会怎么坏,以及你怎么知道它到底有没有用 —— 后半个问题目前没有可以直接引用的公开答案,本篇给出为什么,以及自己该怎么测。
本篇会用到的词:
| 词 | 意思 |
|---|---|
| 记忆投毒 | 诱导系统把伪造的事实写进记忆,之后每一轮都会被读回来 |
| 错误固化 | 一条错记忆影响了回答,这次交互又被抽取成新记忆,把错误加强一次 |
| J score | LoCoMo 论文里用的评判指标,由模型判断回答是否与标准答案一致 |
| 对抗类问题 | LoCoMo 的第 5 类问题,设计上就没有正确答案,协议规定排除在评分之外 |
| 单变量对照 | 一次只改一个组件(模型、嵌入、检索管线)再比较,MemDelta 提出的评测协议 |
一、记忆投毒:三条写入通路
记忆和上下文注入的区别在于持续性:一次上下文注入影响一轮对话,一次记忆投毒影响之后的每一轮。
三条通路的对应防线:
| 通路 | 防线 | 落在哪一层 |
|---|---|---|
| ① 用户直接说 | 记忆分类:涉及凭证、权限、系统配置的内容不允许写入 | 写入前的一个分类器或正则清单 |
| ② 工具结果带指令 | 抽取只从用户消息取(mem0 的 USER_MEMORY_EXTRACTION_PROMPT 就是这个策略),或抽取前剥离工具结果 | 抽取的输入范围 |
| ③ 助手幻觉 | 若抽取范围包含助手消息,加排除清单:不抽对用户的主观刻画、不抽未经用户确认的推断 | 抽取提示词 |
②和抽取范围的取舍直接冲突。02 篇5.3 讲过,只从用户消息抽会丢掉助手给出的方案和承诺;两边都抽则把工具结果和幻觉都拉进了写入面。没有免费的解 —— 现实做法是两边都抽,但对助手消息用更严格的排除清单,并且工具结果单独排除。
外泄方向的威胁(记忆里的东西怎么被套出去)在安全专题 07 篇。两个方向合起来才是完整的记忆威胁面。
二、错误固化
比投毒更常见、也更难发现的是一条无恶意的错记忆自己把自己加强:
可操作的 缓解手段:
- 区分观察与结论。抽取时把「用户这次用了邮件」(观察)和「用户偏好邮件」(结论)分开存,结论类记忆要求至少 N 次独立观察支持,且记下支持它的观察 ID
- 给结论类记忆设置复核期。写入 90 天后降级为"待确认",下次相关交互时由模型显式验证一次
- 让用户能看到并修改。文件式记忆(05 篇)在这一点上有结构性优势 —— 记忆是人可读的文件,用户可以直接翻
三、跨租户串号:三处失守点
这是这一层唯一会被定级为事故的失败模式。三处失守点,按被踩到的频率排序:
| 失守点 | 具体形态 | 防法 |
|---|---|---|
| 检索时漏传租户过滤 | mem0 的 filters 是可选参数,user_id / agent_id / run_id 三个维度漏传任一个,检索范围就扩大 | 在存储层把租户字段做成必填分区键,漏传直接报错。不要只靠代码评审 |
| 共享的记忆命名空间 | LangGraph Store 的 namespace=("memories",) 这类写法,所有用户共用一个命名空间 | 命名空间必须含用户标识:("memories", user_id) |
| 抽取时的租户来源 | 抽取任务异步执行,从队列里取消息时用错了上下文里的 user_id | 租户标识随消息体一起传,不从任何全局或线程局部变量取 |
第二条尤其容易踩,因为官方快速开始的示例就是错的形态:
# ❌ LangMem README 的快速开始写法 —— 所有用户共用一个命名空间
create_manage_memory_tool(namespace=("memories",))
create_search_memory_tool(namespace=("memories",))
# ✅ 命名空间带上用户标识
# 注意:不能只在 search 那边加,manage 那边也要加 —— 否则写进了公共空间,
# 带过滤的搜索反而一条也搜不到,表现成"记忆没生效"而不是"记忆串了"
create_manage_memory_tool(namespace=("memories", user_id))
create_search_memory_tool(namespace=("memories", user_id))
InMemoryStore 同理:README 用它做演示,重启即丢;上生产要换 AsyncPostgresStore。快速开始示例的目的是"三行跑起来",不是"可以照抄上生产" —— 这一条对所有记忆库都成立。
四、评测:为什么公开数字目前不可比
这一节复盘一次完整的公开争议,因为它比任何抽象论述都能说明"记忆评测的数字该怎么读"。
4.1 同一个系统、同一个基准、四个数字
4.2 基准本身的问题
Zep 在复盘里列出的 LoCoMo 数据质量问题:
- 第 5 类(对抗类)缺少标准答案,无法使用 —— 这也正是后来口径之争的来源
- 多模态部分存在图片描述与内容不匹配
- 部分问题的说话人归属标注错误
- 存在有多个合理答案的歧义问题
加上上面那条"全上下文基线更高",结论是:LoCoMo 不足以区分记忆系统的优劣。这不是说它没价值,而是说在它上面赢几个百分点,不构成选型依据。
4.3 MemDelta:把变量一个一个拆开
2026 年 6 月的 MemDelta(arXiv 2606.29914)针对这个问题提出了单变量对照协议:一次只改一个组件,在 LongMemEval-S(500 题、50+ 会话、三个模型家族)上跑。四条结论都值得记住:
| 结论 | 数字 | 对选型的含义 |
|---|---|---|
| 原文切片 RAG 打平全上下文 | 47.2% 对 49.8%,p = 0.34 | 差异不显著。但排序会随模型翻转:Gemini 从全上下文得 +14pp,Sonnet 从 RAG 得 +31pp(部分因为它拒答了 63% 的全上下文查询) |
| 只换嵌入模型 | 准确率变动 +6.2pp,p = 0.004 | 一个变量就能翻转结论:Mem0 比 MiniLM-RAG 高 11pp,但比云端 RAG 低 1.2pp |
| Agent 自管记忆 vs 朴素检索 | 42% 对 47% | 让模型自己决定记什么,在这个设定下不如直接检索原文 |
| Mem0 在 6 类问题中的 2 类(n=88) | 72.7% 对云端 RAG 的 73.9%,p = 1.0,成本 50 倍 | 优势是窄的,不是普遍的 |
论文给出的建议也很直接:记忆评测必须在各组之间固定嵌入模型。上面第二条说明,不固定就等于在比嵌入模型而不是比记忆系统。
五、自己怎么测:一套最小可用评测集
既然公开数字不可比,选型只能自己测。一套能在两天内搭起来的最小集:
# 每条样例的结构:先喂若干轮对话建立记忆,再问一个问题,检查回答里有没有那个事实
# 关键是覆盖 04 篇第二节那四类失效 —— 通用问答集测不出这些
CASES = [
# ① 否定:向量检索最容易取反的一类
{"setup": ["我不吃香菜"], "ask": "推荐一道凉菜", "must_not_contain": ["香菜"]},
# ② 时间:先 说 A 再改成 B,检查旧值有没有被作废
{"setup": ["我住北京", "我上个月搬到上海了"], "ask": "推荐附近的餐厅", "must_contain": ["上海"]},
# ③ 补充 vs 变更:第二条过敏源不能把第一条挤掉(03 篇第一节)
{"setup": ["我对花生过敏", "我还对芒果过敏"], "ask": "我的过敏源有哪些",
"must_contain": ["花生", "芒果"]},
# ④ 多跳:一次检索跨不过去的关系
{"setup": ["我老板是张三", "张三只看邮件"], "ask": "怎么把方案给我老板",
"must_contain": ["邮件"]},
# ⑤ 跨租户:用另一个 user_id 问同一件事,必须问不出来
{"setup": ["我对花生过敏"], "ask_as_other_user": "这个用户对什么过敏",
"must_not_contain": ["花生"]},
# ⑥ 干扰项:库里塞 200 条无关记忆之后,上面每一条还能不能过
# 这一条最重要 —— 空库上全过、满库上大面积失败是最常见的形态
]
评这套集的三条纪律:
- 固定嵌入模型和判分模型,只换记忆实现。这是 MemDelta 的核心建议
- 每个配置跑至少 5 次取平均并报方差。上面那场争议里,双方最终都改用了 10 次运行的平均值
- 必须有"不用记忆"的对照组。如 果记忆层没有显著优于"什么都不做"或"把最近 20 轮全塞进去",那它带来的是成本和故障面,不是能力
第 3 条是最容易被跳过、也最能省钱的一条。